package com.poizon.infrastructure.idem.core.util.impl;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.poizon.infrastructure.idem.core.domain.entity.IdemEntity;
import com.poizon.infrastructure.idem.core.domain.value.enums.IdemReqType;
import com.poizon.infrastructure.idem.core.domain.value.exception.IdempotentDbStoreException;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import static com.poizon.infrastructure.idem.core.domain.value.config.IdemConstants.IDEM_RETRY_WAIT_TIME;

/**
 * 工具类/幂等
 */
@Slf4j
public class IdemUtil extends AbstractIdemUtil {
    public IdemUtil(ThreadPoolExecutor idemAsyncSubmitRespExecutor) {
        super(idemAsyncSubmitRespExecutor);
    }
    /**
     * 幂等操作
     * todo 幂等业务指标监控采集,幂等业务数,成功业务数,失败业务数,并发冲突业务数 DB底层存储异常飞书告警
     * @exception IdempotentDbStoreException 幂等底层存储异常
     * @exception Exception 业务异常
     */
    @Override
    protected Object executeIdemImpl(
            String operationKey,
            Supplier serviceRunnable,
            Long expireTime,
            TimeUnit timeUnit,
            String reqType,
            AtomicInteger retryCount
    ) {
        // 获取幂等key
        String idemKey = Get.getIdemKey(operationKey);

        try {
            // 获取幂等对象
            IdemEntity idemEntity = IdemEntity.getIdemDto(idemKey); // 出现异常直接抛出
            if (idemEntity != null) {
                // 已经存在(不存在并发问题)
                return idemObjHasExist(operationKey, serviceRunnable, expireTime, timeUnit, reqType, retryCount, idemEntity);
            } else {
                // 幂等实体不存在
                return idemObjNotExist(operationKey, serviceRunnable, expireTime, timeUnit, reqType, retryCount);
            }
        } catch (IdempotentDbStoreException e) {
            // 幂等底层存储异常
            log.error("[幂等底层存储异常]", e);
            throw e;
        } catch (Exception e) {
            // 一般对于业务异常,统一异常处理会自动log,不需要重复打
            throw e;
        }
    }


    /**
     * 幂等对象已经存在
     */
    public Object idemObjHasExist(@NotBlank String operationKey, @NotNull Supplier serviceRunnable, @NotNull Long expireTime, @NotNull TimeUnit timeUnit, @NotBlank String reqType, @NotNull AtomicInteger retryCount, @NotNull IdemEntity idemEntity) {
        Object resp = idemEntity.getResp();
        Boolean status = idemEntity.getStatus();

        if (status) {
            // resp已落库
            return idemObjHasExistAndHasResp(resp);
        } else {
            // resp未落库
            if (log.isDebugEnabled()) {
                log.debug("[通用幂等框架] 幂等key={},重复请求,响应未落库,准备重试,重试次数={}", Get.getIdemKey(operationKey), retryCount.get());
            }
            return idemObjHasExistAndNoResp(operationKey, serviceRunnable, expireTime, timeUnit, reqType, retryCount);
        }
    }

    /**
     * 幂等对象不存在,设置空幂等对象
     */
    public Object idemObjNotExist(
            @NotBlank String operationKey,
            @NotNull Supplier serviceRunnable,
            @NotNull Long expireTime,
            @NotNull TimeUnit timeUnit,
            @NotBlank String reqType,
            @NotNull AtomicInteger retryCount
    ) {
        String idemKey = Get.getIdemKey(operationKey);

        // 同步插入唯一键
        IdemEntity emptyRespIdemEntity = IdemEntity.buildCreateIdemDto(idemKey, IdemReqType.valueOf(reqType));

        // 可能出现数据库底层存储异常
        Boolean success = emptyRespIdemEntity.setIdemKey(idemKey, emptyRespIdemEntity, expireTime, timeUnit); // 出现异常直接抛出

        // 并发冲突(设置幂等key)
        if (success) {
            // 可能出现业务异常
            return idemObjNotExistAndNoConcurrencyViolation(idemKey, emptyRespIdemEntity, serviceRunnable, expireTime, timeUnit); // 出现业务异常直接抛出
        } else {
            // 可能出现任何异常
            return idemObjNotExistAndHasConcurrencyViolation(operationKey, serviceRunnable, expireTime, timeUnit, reqType, retryCount, idemKey);

        }
    }

    /**
     * 成功保存初始幂等key入库,准备刷新幂等响应
     * @exception Exception 业务异常
     */
    public Object idemObjNotExistAndNoConcurrencyViolation(
            @NotBlank String idemKey,
            @NotNull IdemEntity emptyRespIdemEntity,
            @NotNull Supplier serviceRunnable,
            @NotNull Long expireTime,
            @NotNull TimeUnit timeUnit
    ) {
        try {
            // 执行业务方法
            Object resp = serviceRunnable.get();
            // 刷新幂等响应入库
            flushIdemResAsync(idemKey, emptyRespIdemEntity, resp, expireTime, timeUnit);
            return resp;
        } catch (Throwable e) {
            log.warn("[通用幂等框架] 幂等key={},业务执行失败,准备删除幂等数据", idemKey);
            try {
                emptyRespIdemEntity.delete(idemKey);
            } catch (Throwable exception) {
                log.error(StrUtil.format("[通用幂等框架] 删除业务执行失败的幂等key={}失败,请检查幂等底层存储是否正常,恢复后手动,请手动删除幂等数据", idemKey), exception);
            }
            // 把业务执行异常抛出
            throw e;
        }
    }

    /**
     * 异步刷新幂等响应数据入库
     */
    public void flushIdemResAsync(@NotBlank String idemKey, @NotNull IdemEntity emptyRespIdem, Object resp, @NotNull Long expireTime, @NotNull TimeUnit timeUnit) {
        // 序列化响应数据(可以采用更高效的压缩存储)
        String respJson = resp == null ? null : JSON.toJSONString(resp);

        CompletableFuture.runAsync(
                () -> {
                    // todo: 可以改造成mq高并发消费,但是考虑框架的轻量化,暂时未实现
                    emptyRespIdem.updateIdemResp(resp, expireTime, timeUnit, idemKey);
                },
                idemAsyncSubmitRespExecutor
        ).exceptionally(( e) -> {
            // 异步写幂等响应失败
            log.error(StrUtil.format("[通用幂等框架] 线程池异步刷新幂等key={},业务响应数据={},入库异常,请求检查线程池状态,开始尝试自动转移到主线程执行", idemKey, respJson), e);

            //自从得物上容器后发现一个严重问题，优雅发布时线程池中的线程还未处理完所有数据，线程池直接被shutdown，
            //导致一旦发版就有部分数据插入失败，这种极少数的极端场景直接用主线程去更新，不借助线程池去操作
            try {
                emptyRespIdem.updateIdemResp(resp, expireTime, timeUnit, idemKey);
            } catch (Exception exception) { // 出现异常人工处理
                log.error(StrUtil.format("[通用幂等框架] 线程池异步刷新幂等key={}失败,请检查幂等底层存储是否正常,恢复后手动,请手动刷新幂等响应结果入库,respJson={}", idemKey, respJson), exception);
            }
            return null;
        });
    }

    /**
     * 幂等对象存在并且存在响应
     */
    public Object idemObjHasExistAndHasResp(Object resp) {
        return resp;
    }

    /**
     * 已经执行过幂等,但是数据没刷(主要体现在数据库),也有可能幂等操作还在执行中,可能执行失败,导致幂等回退,所以要重试
     * 重试包含两部分= 并发插入失败重试 + 幂等建存在,数据不存在重试
     */
    @SneakyThrows
    public Object retry(@NotBlank String operationKey, @NotNull Long expireTime, @NotBlank String reqType, @NotNull Supplier serviceRunnable, @NotNull AtomicInteger retryCount, @NotNull TimeUnit timeUnit) {
        String idemKey = Get.getIdemKey(operationKey);

        log.debug("[通用幂等框架] 幂等key={},并发冲突,休眠重试", idemKey);
        Thread.sleep(IDEM_RETRY_WAIT_TIME);

        int retryTimes = retryCount.getAndDecrement();
        if (retryTimes <= 0) {
            String errorMsg = "[通用幂等框架] 幂等key={},到达重试次数,尚未获取到幂等响应,可能数据落库失败,请人工手动处理";
            log.error(StrUtil.format(errorMsg, idemKey));
            throw new IdempotentDbStoreException(errorMsg);
        }

        log.debug("[通用幂等框架] 幂等key={},开始倒数第{}次重试", idemKey, retryTimes);

        return executeIdemImpl(operationKey, serviceRunnable, expireTime, timeUnit, reqType, retryCount);
    }

    /**
     * 幂等对象存在但是无响应,准备重试
     */
    public Object idemObjHasExistAndNoResp(@NotBlank String operationKey, @NotNull Supplier serviceRunnable, @NotNull Long expireTime, @NotNull TimeUnit timeUnit, @NotBlank String reqType, @NotNull AtomicInteger retryCount) {
        return retry(operationKey, expireTime, reqType, serviceRunnable, retryCount, timeUnit);
    }

    /**
     * 幂等对象存在且存在并发冲突,准备重试
     */
    public Object idemObjNotExistAndHasConcurrencyViolation(String operationKey, Supplier serviceRunnable, Long expireTime, @NotNull TimeUnit timeUnit, @NotBlank String reqType, @NotNull AtomicInteger retryCount, @NotBlank String idemKey) {
        return retry(operationKey, expireTime, reqType, serviceRunnable, retryCount, timeUnit);
    }
}
